﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using DotNetRepository.Tools;

namespace DotNetRepository
{
    public class ManyToManyEntityCollection<TEntity,TManyToManyEntity> : IEntityCollection<TEntity>
        where TEntity : class, IEntity
        where TManyToManyEntity : class, IEntity, new()
    {
        private readonly IEntityCollection<TManyToManyEntity> _innerCollection;
        private readonly Func<TManyToManyEntity, TEntity> _getEntity;
        private readonly Func<TManyToManyEntity, Guid> _getEntityID;
        private readonly Action<TManyToManyEntity, TEntity> _setEntity;
        private readonly bool _saveEntitiesOnSubmit;

        public ManyToManyEntityCollection(ManyToManyEntityCollectionArgs<TEntity,TManyToManyEntity> args)
        {
            if (args.InnerCollection != null)
            {
                _innerCollection = args.InnerCollection;
            }
            else
            {
                // Get the provided foreign key or derive one from the calling method
                var foreignKey = args.InnerCollectionForeignKey ?? GetParentContainerType().Name + "ID";

                _innerCollection = new EntityCollection<TManyToManyEntity>(foreignKey);
            }

            string entityName = null;
            string entityIDName = null;

            if (args.GetEntity == null || args.GetEntityID == null || args.SetEntity == null)
            {
                var membersReferencingEntityType = typeof (TManyToManyEntity).MembersInheritingFrom<TEntity>();
                if (membersReferencingEntityType.Count() == 0)
                    throw new Exception("Unable to find a field or property in " + typeof (TManyToManyEntity).Name +
                                        " that references " + typeof (TEntity).Name + ".");
                if (membersReferencingEntityType.Count() > 1)
                    throw new Exception("Unable to infer the field or property in " + typeof(TManyToManyEntity).Name +
                                        " that references " + typeof(TEntity).Name + ".  More than one result found.");

                entityName = membersReferencingEntityType.Single().Name;

                entityIDName = entityName + "ID";

                if (typeof(TManyToManyEntity).MembersInheritingFrom<Guid>().Where(m=>m.Name == entityIDName).Count()!=1)
                    throw new Exception("Unable to infer the field or property in " + typeof(TManyToManyEntity).Name +
                                        " that references " + typeof(TEntity).Name + ".");
            }

            _getEntity = args.GetEntity ?? (join=>GetEntity(entityName,join));
            _getEntityID = args.GetEntityID ?? (join=>GetEntityID(entityIDName,join));
            _setEntity = args.SetEntity ?? ((join,entity)=>SetEntity(entityName,join,entity));
            _saveEntitiesOnSubmit = args.SaveEntitiesOnSubmit;
        }

        public ManyToManyEntityCollection() : this(new ManyToManyEntityCollectionArgs<TEntity,TManyToManyEntity>())
        { }

        public ManyToManyEntityCollection(IEntityCollection<TManyToManyEntity> innerCollection)
            : this(new ManyToManyEntityCollectionArgs<TEntity, TManyToManyEntity>{InnerCollection = innerCollection})
        { }

        public ManyToManyEntityCollection(IEntityCollection<TManyToManyEntity> innerCollection, bool saveEntitiesOnSubmit)
            : this(new ManyToManyEntityCollectionArgs<TEntity, TManyToManyEntity> { InnerCollection = innerCollection, SaveEntitiesOnSubmit = saveEntitiesOnSubmit })
        { }

        public ManyToManyEntityCollection(bool saveEntitiesOnSubmit)
            : this(new ManyToManyEntityCollectionArgs<TEntity, TManyToManyEntity> { SaveEntitiesOnSubmit = saveEntitiesOnSubmit })
        { }

        private static TEntity GetEntity(string name, TManyToManyEntity join)
        {
            return typeof (TManyToManyEntity).GetValue(name,join) as TEntity;
        }

        private static Guid GetEntityID(string name, TManyToManyEntity join)
        {
            return (Guid)typeof(TManyToManyEntity).GetValue(name,join);
        }

        private static void SetEntity(string name, TManyToManyEntity join, TEntity entity)
        {
            typeof (TManyToManyEntity).SetValue(name, join, entity);
        }

        private static Type GetParentContainerType()
        {
            var stackTrace = new StackTrace();
            try
            {
                // Start at frame 2 (frame 0 is this method, frame 1 is the method that called this)
                for (var i = 2; i < stackTrace.FrameCount; i++)
                {
                    var targetType = stackTrace.GetFrame(i).GetMethod().ReflectedType;
                    if (targetType.GetInterfaces().Contains(typeof(IEntity)))
                        return targetType;
                }
                throw new Exception("Could not derive container type.");
            }
            catch (Exception ex)
            {
                throw new Exception("Could not derive container type.");
            }
        }

        public IEnumerator<TEntity> GetEnumerator()
        {
            return _innerCollection.Select(manyToMany => _getEntity(manyToMany)).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public void OnEntityUpdated(IEntity entity)
        {
            var manyToMany = entity as TManyToManyEntity;
            if (manyToMany != null)
                _innerCollection.OnEntityUpdated(manyToMany);
        }

        public void OnEntityDeleted(Guid entityID)
        {
            if (_innerCollection.Contains(entityID))
            {
                _innerCollection.OnEntityDeleted(entityID);
            }
        }

        public void SubmitChanges()
        {
            if (_saveEntitiesOnSubmit)
            {
                foreach (var manyToMany in _innerCollection)
                {
                    var entity = _getEntity(manyToMany);
                    Repository<TEntity>.Save(entity);
                }
            }
            _innerCollection.SubmitChanges();
        }

        public void RevertChanges()
        {
            _innerCollection.RevertChanges();
        }

        public void ClearAndSubmit()
        {
            _innerCollection.ClearAndSubmit();
        }

        public bool Initialized
        {
            get { return (!_saveEntitiesOnSubmit) && _innerCollection.Initialized; }
        }

        public void Add(TEntity item)
        {
            var manyToMany = new TManyToManyEntity();
            _setEntity(manyToMany, item);
            _innerCollection.Add(manyToMany);
        }

        public void Clear()
        {
            _innerCollection.Clear();
        }

        public bool Contains(TEntity item)
        {
            return _innerCollection.Where(i => _getEntity(i) == item).Count() > 0;
        }

        public bool ContainsAny(IEnumerable<TEntity> items)
        {
            return _innerCollection.Where(i => items.Contains(_getEntity(i))).Count() > 0;
        }

        public bool ContainsAll(IEnumerable<TEntity> items)
        {
            return _innerCollection.Where(i => items.Contains(_getEntity(i))).Count() == items.Count();
        }

        public void Initialize(object parentContainer)
        {
            Initialize(parentContainer as IEntity);
        }

        public void Initialize(IEntity parentContainer)
        {
            if (Initialized) return;
            if (parentContainer == null) throw new ArgumentNullException("parentContainer");

            if (!_innerCollection.Initialized)
            {
                _innerCollection.Initialize(parentContainer);    
            }
        }

        public void CopyTo(TEntity[] array, int arrayIndex)
        {
            _innerCollection.Select(i=>_getEntity(i)).ToList().CopyTo(array,arrayIndex);
        }

        public bool Remove(TEntity item)
        {
            var entityToRemove = _innerCollection.Where(i => _getEntity(i) == item).FirstOrDefault();
            return entityToRemove != null && _innerCollection.Remove(entityToRemove);
        }

        public int RecordCount
        {
            get { return _innerCollection.RecordCount; }
        }

        public int Count
        {
            get { return _innerCollection.Count; }
        }

        public bool IsReadOnly
        {
            get { return _innerCollection.IsReadOnly; }
        }

        public TEntity this[Guid id]
        {
            get { return _innerCollection.Where(i => _getEntityID(i) == id).Select(i => _getEntity(i)).FirstOrDefault(); }
        }

        public bool Contains(Guid id)
        {
            return _innerCollection.Where(i => _getEntityID(i) == id).Count() > 0;
        }

        public bool ContainsAny(IEnumerable<Guid> ids)
        {
            return _innerCollection.Where(i => ids.Contains(_getEntityID(i))).Count() > 0;
        }

        public bool ContainsAll(IEnumerable<Guid> ids)
        {
            return _innerCollection.Where(i => ids.Contains(_getEntityID(i))).Count() == ids.Count();
        }

        public bool Remove(Guid id)
        {
            var entityToRemove = _innerCollection.FirstOrDefault(i => _getEntityID(i) == id);
            return entityToRemove != null && _innerCollection.Remove(entityToRemove);
        }

        public event EntityAddedEventHandler<TEntity> EntityAddedEvent;
        public event EntityRemovedEventHandler<TEntity> EntityRemovedEvent;
    }
}
